/* 
 * A config file parser. Copyright (c) Michael Still (mikal@stillhq.com) 2004, 
 * released under the terms of the GNU GPL version 2 
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>

#include "config.h"

#ifdef TESTING

int main(int argc, char *argv[])
{
  config_state *cfg;
  char *val;

  /* 
   * This will result in the following config files being checked for existance:
   *    - /etc/testing
   *    - ~/.testing
   *    - ./testing
   */
  cfg = config_open("testing");
  if(!cfg)
    {
      printf("Config file reading failed\n");
      return 2;
    }

  /*
   * Now we can read values from that config file
   */
  printf("A banana is a %s\n", config_getstring(cfg, "banana"));

  /*
   * Now we can close the config file just like a normal file
   */
  config_close(cfg);
}

#endif

config_state *config_open(char *name)
{
  int fd;
  config_state *cfg;
  struct stat sb;

  cfg = malloc(sizeof(config_state));
  if(!cfg)
    {
      perror("config: could not allocate a config structure");
      return NULL;
    }
  
  fd = config_file_open("/etc", name, 0);

  if(fd == -1)
    {
      char *home = getenv("HOME");
      fd = config_file_open(home, name, 1);
//      if(home)
//	free(home);
    }


  if(fd == -1)
    {
      char *fname = malloc(strlen(name) + 5);
      if(!fname)
	{
	  perror("config: could not allocate memory for config filename");
	  free(cfg);
	  return NULL;
	}
      snprintf(fname, strlen(name) + 5, "%s.cfg", name);
      fd = config_file_open(".", fname, 0);
      free(fname);
    }

  if(fd == -1)
    {
      free(cfg);
      return NULL;
    }

  /*
   * We return a mmapped view of the file
   */

  if(fstat(fd, &sb) < 0)
    {
      perror("config: could not stat config file");
      close(fd);
      free(cfg);
      return NULL;
    }

  cfg->size = sb.st_size;
  if((int) (cfg->file = (char *) mmap(NULL, cfg->size, PROT_READ | PROT_WRITE, 
				      MAP_PRIVATE, fd, 0)) == -1)
    {
      perror("config: could not mmap config file");
      close(fd);
      free(cfg);
      return NULL;
    }

  /*
   * We prebuild a list of the config strings in this file, to save
   * repeatedly parsing the file over and over later
   */
  cfg->lines = config_parse(cfg->file);
  return cfg;
}

void ll_free(struct config_list **ll_pos)
{
    struct config_list *ll_tofree;

    while ((*ll_pos) != NULL)
    {
        ll_tofree = (*ll_pos);
        (*ll_pos) = (*ll_pos)->next;

        // clean up the node
        ll_tofree->next = NULL;
        free(ll_tofree->line);
        free(ll_tofree->key);
        free(ll_tofree->value);
        free(ll_tofree);
        ll_tofree = NULL;
    }
}

void config_close(config_state *cfg)
{
    if(cfg->file)
    {
        munmap(cfg->file, cfg->size);
    }

    if(cfg->lines != NULL)
    {
        ll_free(&(cfg->lines));
    }
}

int config_file_open(char *dir, char *name, int dotted)
{
  char *path;
  int fd, chars;
  
  chars = strlen(dir) + strlen(name) + 3;
  path = malloc(sizeof(char) * chars);
  if(path == NULL)
    {
      perror("config: couldn't allocate memory to open config file");
      return -1;
    }

  snprintf(path, chars, "%s/%s%s", dir, (dotted ? "." : ""), name);
  fd = open(path, O_RDONLY);

  free(path);
  return fd;
}

void ll_add(struct config_list **head, struct config_list *to_add)
{
    struct config_list *cur_pos = *head;

    if (*head == NULL)
    {
        *head = to_add;
    }
    else
    {
        // move to the end of the list
        while (cur_pos->next != NULL)
            cur_pos = cur_pos->next;

        cur_pos->next = to_add;
    }
}


struct config_list *config_parse(char *orig_file)
{
    struct config_list *head = NULL, *p;
    char *line;

    // strtok will mod the file so dup it first
    char *file = strdup(orig_file);


  /*
   * Work through the file and build a linked list of lines
   */

    line = strtok(file, "\n");

    do
    {
        // skip some of the blank lines
        if( strlen(line) < 4 )
            continue;
        // skip lines that don't have the = token
//        if( strchr(line,'=') == NULL)
//            continue;

        p = (struct config_list *) malloc(sizeof(struct config_list));
        if(!p)
	  return NULL;
      
        // low cost to just dup the line 3 times to make sure there is enough space.
        p->line = strdup(line);
        p->key = strdup(line);
        p->value = strdup(line);
        p->next = NULL;

        // if we don't find two strings there is an error on this line
        if ( sscanf(p->line,"%s%s", p->key, p->value) != 2)
        {
            free(p->value);
            free(p->key);
            free(p->line);
            free(p);
            continue;
        }

        // add to tail of linked list
        ll_add(&head, p);

    } while((line = strtok(NULL, "\n")) != NULL);

  return (head);
}


char *config_getstring(config_state *cfg, char *key)
{
    struct config_list *cur_pos;

    for(cur_pos=cfg->lines; cur_pos != NULL;  cur_pos = cur_pos->next)
    {
        if(strcmp(cur_pos->key, key) == 0)
        {
	    return (cur_pos->value);
	}
    }

  return (NULL);
}
